package com.liuqianfei.mdurl;

import com.liuqianfei.util.ArrayUtil;
import com.liuqianfei.util.RegExpUtil;
import com.liuqianfei.util.StringUtil;

import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @author liuqianfei
 */
public class MdUrl
{
    public String protocol;
    public boolean slashes;
    public String auth;
    public String port;
    public String hostname;
    public String hash;
    public String search;
    public String pathname;

    String PROTOCOL_PATTERN = "^([a-z0-9.+-]+:)";    // ignore case
    String PORT_PATTERN = ":[0-9]*$";
    // Special case for a simple path URL
    String SIMPLE_PATH_PATTERN = "^(\\/\\/?(?!\\/)[^\\?\\s]*)(\\?[^\\s]*)?$";

    // RFC 2396: characters reserved for delimiting URLs.
    // We actually just auto-escape these.
    char[] delims = {'<', '>', '"', '`', ' ', '\r', '\n', '\t'};

    // RFC 2396: characters not allowed for various reasons.
    char[] unwise;

    // Allowed by RFCs, but cause of XSS attacks.  Always escape these.
    char[] autoEscape;

    // Characters that are never ever allowed in a hostname.
    // Note that any invalid chars are also handled, but these
    // are the ones that are *expected* to be seen, so we fast-path
    // them.
    char[] nonHostChars;
    char[] hostEndingChars = { '/', '?', '#' };

    int hostnameMaxLen = 255;
    String HOSTNAME_PART_PATTERN = "^[+a-z0-9A-Z_-]{0,63}$";
    String HOSTNAME_PART_START = "^([+a-z0-9A-Z_-]{0,63})(.*)$";

    // protocols that can allow "unsafe" and "unwise" chars.
    /* eslint-disable no-script-url */
    // protocols that never have a hostname.
    Map<String, Boolean> hostlessProtocol = new HashMap<>();

    // protocols that always contain a // bit.
    Map<String, Boolean> slashedProtocol = new HashMap<>();

    public MdUrl()
    {
        unwise = ArrayUtil.concat(delims, new char[]{'{', '}', '|', '\\', '^', '`'});

        autoEscape = ArrayUtil.concat(unwise, new char[]{'\''});

        nonHostChars = ArrayUtil.concat(autoEscape, new char[]{ '%', '/', '?', ';', '#' });

        hostlessProtocol.put("javascript", true);
        hostlessProtocol.put("javascript:", true);

        slashedProtocol.put("http", true);
        slashedProtocol.put("https", true);
        slashedProtocol.put("ftp", true);
        slashedProtocol.put("gopher", true);
        slashedProtocol.put("file", true);
        slashedProtocol.put("http:", true);
        slashedProtocol.put("https:", true);
        slashedProtocol.put("ftp:", true);
        slashedProtocol.put("gopher:", true);
        slashedProtocol.put("file:", true);
    }


    public MdUrl parse(String url, boolean slashesDenoteHost)
    {
        if (StringUtil.isEmpty(url))
            return new MdUrl();

        int i, l;
        String lowerProto = null;
        int hec;
        boolean slashes = false;
        String rest = url;

        // trim before proceeding.
        // This is to support parse stuff like "  http://foo.com  \n"
        rest = rest.trim();

        if (!slashesDenoteHost && url.split("#").length == 1)
        {
            Pattern simplePathPattern = Pattern.compile(SIMPLE_PATH_PATTERN);
            Matcher simplePathMatcher = simplePathPattern.matcher(rest);
            int matchCount = simplePathMatcher.groupCount();
            // Try fast path regexp

            if (simplePathMatcher.find() && matchCount > 0)
            {
                this.pathname = simplePathMatcher.group(1);
                if (matchCount > 1)
                {
                    this.search = simplePathMatcher.group(2);
                }
                return this;
            }
        }

        Pattern protocolPattern = Pattern.compile(PROTOCOL_PATTERN, Pattern.CASE_INSENSITIVE);
        Matcher protocalMatcher = protocolPattern.matcher(rest);
        String proto = null;
        if (protocalMatcher.find())
        {
            proto = protocalMatcher.group();
            lowerProto = proto.toLowerCase();
            this.protocol = proto;
            rest = StringUtil.substr(rest, proto.length());
        }

        // figure out if it's got a host
        // user@server is *always* interpreted as a hostname, and url
        // resolution will treat //foo/bar as host=foo,path=bar because that's
        // how the browser resolves relative URLs.
        String p1 = "^\\/\\/[^@\\/]+@[^@\\/]+";
        Pattern pattern1 = Pattern.compile(p1);
        Matcher m1 = pattern1.matcher(rest);
        if (slashesDenoteHost || StringUtil.isNotEmpty(proto) || m1.find())
        {
            slashes = "//".equals(StringUtil.substr(rest, 0, 2));
            if (slashes && !(StringUtil.isNotEmpty(proto) && null != hostlessProtocol.get(proto) && hostlessProtocol.get(proto)))
            {
                rest = StringUtil.substr(rest, 2);
                this.slashes = true;
            }
        }

        if (null != hostlessProtocol.get(proto) && !hostlessProtocol.get(proto) && (slashes || (StringUtil.isNotEmpty(proto) && !slashedProtocol.get(proto))))
        {

            // there's a hostname.
            // the first instance of /, ?, ;, or # ends the host.
            //
            // If there is an @ in the hostname, then non-host chars *are* allowed
            // to the left of the last @ sign, unless some host-ending character
            // comes *before* the @-sign.
            // URLs are obnoxious.
            //
            // ex:
            // http://a@b@c/ => user:a@b host:c
            // http://a@b?@c => user:a host:c path:/?@c

            // v0.12 TODO(isaacs): This is not quite how Chrome does things.
            // Review our test case against browsers more comprehensively.

            // find the first instance of any hostEndingChars
            int hostEnd = -1;
            for (i = 0; i < hostEndingChars.length; i++)
            {
                hec = rest.indexOf(hostEndingChars[i]);
                if (hec != -1 && (hostEnd == -1 || hec < hostEnd))
                {
                    hostEnd = hec;
                }
            }

            // at this point, either we have an explicit point where the
            // auth portion cannot go past, or the last @ char is the decider.
            String auth;
            int atSign;
            if (hostEnd == -1)
            {
                // atSign can be anywhere.
                atSign = rest.lastIndexOf('@');
            }
            else
            {
                // atSign must be in auth portion.
                // http://a@b/c@d => host:b auth:a path:/c@d
                atSign = rest.lastIndexOf('@', hostEnd);
            }

            // Now we have a portion which is definitely the auth.
            // Pull that off.
            if (atSign != -1)
            {
                auth = StringUtil.slice(rest, 0, atSign);
                rest = StringUtil.slice(rest, atSign + 1);
                this.auth = auth;
            }

            // the host is the remaining to the left of the first non-host char
            hostEnd = -1;
            for (i = 0; i < nonHostChars.length; i++)
            {
                hec = rest.indexOf(nonHostChars[i]);
                if (hec != -1 && (hostEnd == -1 || hec < hostEnd))
                {
                    hostEnd = hec;
                }
            }
            // if we still have not hit it, then the entire thing is a host.
            if (hostEnd == -1)
            {
                hostEnd = rest.length();
            }

            if (rest.charAt(hostEnd - 1) == ':') { hostEnd--; }
            String host = StringUtil.slice(rest, 0, hostEnd);
            rest = StringUtil.slice(rest, hostEnd);

            // pull out port.
            this.parseHost(host);

            // we've indicated that there is a hostname,
            // so even if it's empty, it has to be present.
            if (StringUtil.isEmpty(this.hostname))
                this.hostname = "";

            // if hostname begins with [ and ends with ]
            // assume that it's an IPv6 address.
            boolean ipv6Hostname = this.hostname.charAt(0) == '[' && this.hostname.charAt(this.hostname.length() - 1) == ']';

            // validate a little.
            if (!ipv6Hostname)
            {
                String[] hostparts = this.hostname.split("\\.");
                for (i = 0, l = hostparts.length; i < l; i++)
                {
                    String part = hostparts[i];
                    if (StringUtil.isEmpty(part))
                        continue;

                    Pattern hostnamePartPattern = Pattern.compile(HOSTNAME_PART_PATTERN);
                    Matcher hostnamePartMatcher = hostnamePartPattern.matcher(part);
                    if (!hostnamePartMatcher.find())
                    {
                        String newpart = "";
                        for (int j = 0, k = part.length(); j < k; j++)
                        {
                            if (part.charAt(j) > 127)
                            {
                                // we replace non-ASCII char with a temporary placeholder
                                // we need this to make sure size of hostname is not
                                // broken by replacing non-ASCII by nothing
                                newpart += 'x';
                            }
                            else
                            {
                                newpart += part.charAt(j);
                            }
                        }
                        // we test again with ASCII char only
                        Pattern hostnamePartPattern2 = Pattern.compile(HOSTNAME_PART_PATTERN);
                        Matcher hostnamePartMatcher2 = hostnamePartPattern.matcher(newpart);
                        if (!hostnamePartMatcher2.find())
                        {
                            String[] validParts = ArrayUtil.slice(hostparts, 0, i);
                            String[] notHost = ArrayUtil.slice(hostparts, i + 1);

                            if (RegExpUtil.match(HOSTNAME_PART_START, part, false))
                            {
                                validParts = ArrayUtil.push(validParts, RegExpUtil.group(HOSTNAME_PART_START, part, false, 1));
                                notHost = ArrayUtil.unshift(notHost, RegExpUtil.group(HOSTNAME_PART_START, part, false, 2));
                            }
                            if (notHost.length > 0)
                                rest = String.join(".", notHost) + rest;

                            this.hostname = String.join(".", validParts);
                            break;
                        }
                    }
                }
            }

            if (this.hostname.length() > hostnameMaxLen)
                this.hostname = "";

            // strip [ and ] from the hostname
            // the host field still retains them, though
            if (ipv6Hostname)
                this.hostname = StringUtil.substr(this.hostname, 1, this.hostname.length() - 2);
        }

        // chop off from the tail first.
        int hash = rest.indexOf('#');
        if (hash != -1)
        {
            // got a fragment string.
            this.hash = StringUtil.substr(rest, hash);
            rest = StringUtil.slice(rest, 0 ,hash);
        }

        int qm = rest.indexOf('?');
        if (qm != -1)
        {
            this.search = StringUtil.substr(rest, qm);
            rest = StringUtil.slice(rest, 0, qm);
        }

        if (StringUtil.isNotEmpty(rest))
            this.pathname = rest;

        if (null != lowerProto && slashedProtocol.get(lowerProto) && StringUtil.isNotEmpty(this.hostname) && StringUtil.isEmpty(this.pathname))
            this.pathname = "";

        return this;
    };

    public void parseHost(String host)
    {
        String port = RegExpUtil.group(PORT_PATTERN, host, false, 0);

        if (StringUtil.isNotEmpty(port))
        {
            if (!":".equals(port))
                this.port = StringUtil.substr(port, 1);

            host = StringUtil.substr(host, 0, host.length() - port.length());
        }

        if (StringUtil.isNotEmpty(host))
            this.hostname = host;
    }

    @Override
    public String toString()
    {
        return "MdUrl{" +
                "protocol='" + protocol + '\'' +
                ", slashes=" + slashes +
                ", auth='" + auth + '\'' +
                ", port='" + port + '\'' +
                ", hostname='" + hostname + '\'' +
                ", hash='" + hash + '\'' +
                ", search='" + search + '\'' +
                ", pathname='" + pathname + '\'' +
                '}';
    }
}
